//**********************************************************************
//
//<copyright>
//
//BBN Technologies
//10 Moulton Street
//Cambridge, MA 02138
//(617) 873-8000
//
//Copyright (C) BBNT Solutions LLC. All rights reserved.
//
//</copyright>
//**********************************************************************
//
//$Source:
///cvs/darwars/ambush/aar/src/com/bbn/ambush/mission/MissionHandler.java,v
//$
//$RCSfile: GeoArray.java,v $
//$Revision: 1.2 $
//$Date: 2007/02/13 20:02:10 $
//$Author: dietrick $
//
//**********************************************************************
package com.bbn.openmap.geo;


/**
 * A GeoArray is a interface that represents a set of Geo information. Rather
 * than keeping a set of Geo[] around and managing the memory for all of those
 * objects, the GeoArray provides an object that just holds onto the coordinates
 * of those points.
 * 
 * @author dietrick
 */
public interface GeoArray {

   /**
    * Get a Geo represented by the index i.
    * 
    * @param i
    * @return Geo at index i
    */
   Geo get(int i);

   /**
    * Load the values for Geo at index i into ret.
    * 
    * @param i
    * @param ret
    * @return ret filled in with values at index i
    */
   Geo get(int i, Geo ret);

   /**
    * Get the number of Geo points represented by this array.
    * 
    * @return number of geo points
    */
   int getSize();

   /**
    * Convert the GeoArray to an array of Geos.
    * 
    * @return Geo array from values
    */
   Geo[] toPointArray();

   /**
    * Convert the GeoArray to an array of decimal degree values, alternating
    * lat, lon, lat, lon.
    * 
    * @return lat,lon array representing decimal degrees of points.
    */
   double[] toLLDegrees();

   /**
    * Convert the GeoArray to an array of radian values, alternating lat, lon,
    * lat, lon.
    * 
    * @return lat, lon array representing radians for points.
    */
   double[] toLLRadians();

   /**
    * @param index the index of the Geo in the GeoArray to compare.
    * @param comp the Geo to compare to the indexed value.
    * @return true of x, y, and z of the Geos match.
    */
   boolean equals(int index, Geo comp);

   /**
    * Returns the perpendicular distance to the closest point on the edge of the
    * polygon.
    * 
    * @param geo the point to test against the poly
    * @param closestPoint will be filled with location of poly edge point closest to geo.
    * @return the distance in radians
    */
   double distance(Geo geo, Geo closestPoint);

   /**
    * Compute the area of the GeoArray polygon on the surface of a unit sphere
    * given an enumeration of its point. For a non unit sphere, multiply this by
    * the radius of sphere squared.
    * 
    * @return area value.
    */
   double area();

   /**
    * Ensure that the Geo array starts and ends with the same values. Will
    * replace the current coord array with one three floats longer if needed.
    */
   void closeArray();

   /**
    * Modify, if needed, the Geo array with the duplicates removed.
    */
   void removeDups();

   /**
    * A Mutable GeoArray is one where the points can be modified.
    * 
    * @author dietrick
    */
   public static interface Mutable
         extends GeoArray {

      /**
       * Set the values for the provided index to the values represented by g.
       * 
       * @param i
       * @param g
       */
      void set(int i, Geo g);

      /**
       * Set the values for the provided index to the values x, y, z, which are
       * vector Geo values, *not* lat, lon and height.
       * 
       * @param i
       * @param x
       * @param y
       * @param z
       */
      void set(int i, double x, double y, double z);

      /**
       * Set the values for the provided index to the latitude, longitude.
       * 
       * @param i
       * @param lat
       * @param lon
       * @param isDegrees true if lat/lon in decimal degrees.
       */
      void set(int i, double lat, double lon, boolean isDegrees);
   }

   /**
    * An abstract parent implementation class of GeoArray that handles common
    * methods.
    * 
    * @author dietrick
    */
   public static abstract class Adapter
         implements GeoArray {

      /**
       * Convert the GeoArray to an array of Geos.
       * 
       * @return Geo[]
       */
      public Geo[] toPointArray() {
         int size = getSize();
         Geo[] geos = new Geo[size];
         for (int i = 0; i < size; i++) {
            geos[i] = get(i, new Geo());
         }
         return geos;
      }

      /**
       * Convert the GeoArray to an array of decimal degree values, alternating
       * lat, lon, lat, lon.
       * 
       * @return lat/lon double[] of decimal degrees
       */
      public double[] toLLDegrees() {
         int size = getSize();
         double[] coords = new double[size * 2];
         Geo storage = new Geo();
         for (int i = 0; i < size; i++) {
            get(i, storage);
            int loc = i * 2;
            coords[loc] = storage.getLatitude();
            coords[loc + 1] = storage.getLongitude();
         }
         return coords;
      }

      /**
       * Convert the GeoArray to an array of radian values, alternating lat,
       * lon, lat, lon.
       * 
       * @return lat/lon double[] of radians
       */
      public double[] toLLRadians() {
         int size = getSize();
         double[] coords = new double[size * 2];
         Geo storage = new Geo();
         for (int i = 0; i < size; i++) {
            get(i, storage);
            int loc = i * 2;
            coords[loc] = storage.getLatitudeRadians();
            coords[loc + 1] = storage.getLongitudeRadians();
         }
         return coords;
      }

      /**
       * Computes the area of a polygon on the surface of a unit sphere. For a
       * non unit sphere, multiply this by the radius of sphere squared. The
       * value might be negative based on the counter-clockwise order of the
       * coordinates, but the absolute value is valid. This method will test for
       * closed polygon coordinates and handle that situation.
       */
      public double area() {
         int count = 0;
         double area = 0;
         Geo v0 = get(0, new Geo());
         Geo v1 = get(1, new Geo());
         Geo p0 = new Geo(v0);
         Geo p1 = new Geo(v1);
         Geo p2 = get(getSize() - 1, new Geo());
         // Having the first and last points the same messes up the
         // algorithm.
         // So skip the last point if it equals the first.
         boolean closed = p0.equals(p2);
         int size = getSize() - (closed ? 1 : 0);
         for (int i = 2; i < size; i++) {
            count++;
            get(i, p2);
            area += Geo.angle(p0, p1, p2);
            p0.initialize(p1);
            p1.initialize(p2);
         }

         count++;
         p2.initialize(v0);
         area += Geo.angle(p0, p1, p2);
         p0.initialize(p1);
         p1.initialize(p2);

         count++;
         p2.initialize(v1);
         area += Geo.angle(p0, p1, p2);

         return area - ((count - 2) * Math.PI);
      }

      /**
       * Returns the perpendicular distance to the closest point on the edge of
       * the polygon.
       * 
       * @param pnt the point to test against the poly
       * @param closestPoint if not null, will be set with the location of the
       *        point on the poly closest to pnt, you can read this object after
       *        this method call to get coordinates.
       * @return the distance in radians, or Double.POSITIVE_INFINITY if
       *         something weird happens.
       */
      public double distance(Geo pnt, Geo closestPoint) {
         double ret = java.lang.Double.POSITIVE_INFINITY;
         double testDist = ret;
         int size = getSize();
         Geo p0 = get(0, new Geo());
         Geo p1 = new Geo();
         Geo intersect = new Geo();

         for (int i = 1; i < size; i++) {
            get(i, p1);

            // Don't want to do distance test if end points are the same.
            if (p0.equals(p1)) {
               continue;
            }

            // The test needs to check two things - the distance between pnt and
            // the great circle line between p1 and p2. It should also check to
            // make sure that the perpendicular line intersects that great
            // circle line between p0 and p1. We'll calculate the distance
            // first, and then if the distance is the shortest seen so far,
            // we'll test the distance between that intersection point and make
            // sure that it's less than the distance between the two points.

            testDist = Intersection.pointCircleDistance(p0, p1, pnt);
            System.out.println("testing " + p0 + ", " + p1 + ", getting distance of " + testDist);

            if (testDist < ret) {
               // Find the point where the perpendicular line intersects the
               // great circle
               intersect = p0.intersect(p1, pnt, intersect);
               System.out.println("candidate received, gc intersected at " + intersect);

               if (Intersection.isOnSegment(p0, p1, intersect)) {
                  // Shortest distance, and between points
                  ret = testDist;
                  if (closestPoint != null) {
                     closestPoint.initialize(intersect);
                  }
               }
            }

            // Move to next point in array.
            p0.initialize(p1);
         }

         return ret;
      }
   }

   /**
    * An implementation of GeoArray and GeoArray.Mutable that contains
    * float-precision values. Holds the coordinates in a float array of x, y, z,
    * x, y, z values.
    * 
    * @author dietrick
    */
   public static class Float
         extends Adapter
         implements Mutable {

      private float[] coords;

      public Float(Geo[] geos) {
         coords = new float[geos.length * 3];
         for (int i = 0; i < geos.length; i++) {
            int loc = i * 3;
            Geo geo = geos[i];
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }
      }

      public Float(GeoArray ga) {
         int size = ga.getSize();
         coords = new float[size * 3];
         Geo geo = new Geo();
         for (int i = 0; i < size; i++) {
            int loc = i * 3;
            ga.get(i, geo);
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }
      }

      protected Float(float[] coords) {
         this.coords = coords;
      }

      public static Float createFromLatLonDegrees(float[] latlondeg) {
         int numCoordSets = latlondeg.length / 2;
         float[] coords = new float[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initialize(latlondeg[i * 2], latlondeg[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }

         return new Float(coords);
      }

      public static Float createFromLatLonDegrees(double[] latlondeg) {
         int numCoordSets = latlondeg.length / 2;
         float[] coords = new float[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initialize(latlondeg[i * 2], latlondeg[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }

         return new Float(coords);
      }

      public static Float createFromLatLonRadians(float[] latlonrad) {
         int numCoordSets = latlonrad.length / 2;
         float[] coords = new float[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initializeRadians(latlonrad[i * 2], latlonrad[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }

         return new Float(coords);
      }

      public static Float createFromLatLonRadians(double[] latlonrad) {
         int numCoordSets = latlonrad.length / 2;
         float[] coords = new float[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initializeRadians(latlonrad[i * 2], latlonrad[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = (float) geo.x();
            coords[loc + 1] = (float) geo.y();
            coords[loc + 2] = (float) geo.z();
         }

         return new Float(coords);
      }

      public static Float createFromGeoCoords(float[] xyz) {
         return new Float(xyz);
      }

      public int getSize() {
         if (coords != null) {
            return coords.length / 3;
         }

         return 0;
      }

      public void set(int i, double x, double y, double z) {
         int loc = i * 3;
         coords[loc] = (float) x;
         coords[loc + 1] = (float) y;
         coords[loc + 2] = (float) z;
      }

      public void set(int i, Geo g) {
         set(i, g.x(), g.y(), g.z());
      }

      public void set(int i, double lat, double lon, boolean isDegrees) {
         set(i, new Geo(lat, lon, isDegrees));
      }

      public Geo get(int i) {
         return get(i, new Geo());
      }

      public Geo get(int i, Geo ret) {
         int loc = i * 3;
         double x = coords[loc];
         double y = coords[loc + 1];
         double z = coords[loc + 2];
         ret.initialize(x, y, z);
         return ret;
      }

      public boolean equals(int index, Geo comp) {
         int loc = index * 3;
         double x = coords[loc];
         double y = coords[loc + 1];
         double z = coords[loc + 2];
         return x == comp.x() && y == comp.y() && z == comp.z();
      }

      /**
       * Ensure that the Geo array starts and ends with the same values. Will
       * replace the current coord array with one three floats longer if needed.
       */
      public void closeArray() {
         int l = coords.length;
         int i = l - 3;
         if (coords[0] != coords[i] || coords[1] != coords[i + 1] || coords[2] != coords[i + 2]) {
            float[] newCoords = new float[l + 3];
            System.arraycopy(coords, 0, newCoords, 0, l);
            newCoords[l] = coords[0];
            newCoords[l + 1] = coords[1];
            newCoords[l + 2] = coords[2];
            coords = newCoords;
         }
      }

      /**
       * Modify, if needed, the Geo array with the duplicates removed.
       */
      public void removeDups() {
         Geo[] ga = toPointArray();
         Geo[] r = new Geo[ga.length];
         int p = 0;
         for (int i = 0; i < ga.length; i++) {
            if (p == 0 || !(r[p - 1].equals(ga[i]))) {
               r[p] = ga[i];
               p++;
            }
         }

         if (p != ga.length) {
            coords = new float[p * 3];
            for (int i = 0; i < p; i++) {
               int loc = i * 3;
               Geo geo = r[i];
               coords[loc] = (float) geo.x();
               coords[loc + 1] = (float) geo.y();
               coords[loc + 2] = (float) geo.z();
            }
         }
      }
   }

   /**
    * An implementation of GeoArray and GeoArray.Mutable that contains
    * double-precision values. Holds the coordinates in a double array of x, y,
    * z, x, y, z values.
    * 
    * @author dietrick
    */
   public static class Double
         extends Adapter
         implements Mutable {

      private double[] coords;

      public Double(Geo[] geos) {
         coords = new double[geos.length * 3];
         for (int i = 0; i < geos.length; i++) {
            int loc = i * 3;
            Geo geo = geos[i];
            coords[loc] = geo.x();
            coords[loc + 1] = geo.y();
            coords[loc + 2] = geo.z();
         }
      }

      public Double(GeoArray ga) {
         int size = ga.getSize();
         coords = new double[size * 3];
         Geo geo = new Geo();
         for (int i = 0; i < size; i++) {
            int loc = i * 3;
            ga.get(i, geo);
            coords[loc] = geo.x();
            coords[loc + 1] = geo.y();
            coords[loc + 2] = geo.z();
         }
      }

      protected Double(double[] coords) {
         this.coords = coords;
      }

      public static Double createFromLatLonDegrees(double[] latlondeg) {
         int numCoordSets = latlondeg.length / 2;
         double[] coords = new double[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initialize(latlondeg[i * 2], latlondeg[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = geo.x();
            coords[loc + 1] = geo.y();
            coords[loc + 2] = geo.z();
         }

         return new Double(coords);
      }

      public static Double createFromLatLonRadians(double[] latlonrad) {
         int numCoordSets = latlonrad.length / 2;
         double[] coords = new double[numCoordSets * 3];
         Geo geo = new Geo();
         for (int i = 0; i < numCoordSets; i++) {
            geo.initializeRadians(latlonrad[i * 2], latlonrad[i * 2 + 1]);
            int loc = i * 3;
            coords[loc] = geo.x();
            coords[loc + 1] = geo.y();
            coords[loc + 2] = geo.z();
         }

         return new Double(coords);
      }

      public static Double createFromGeoCoords(double[] xyz) {
         return new Double(xyz);
      }

      public int getSize() {
         if (coords != null) {
            return coords.length / 3;
         }

         return 0;
      }

      public void set(int i, double x, double y, double z) {
         int loc = i * 3;
         coords[loc] = x;
         coords[loc + 1] = y;
         coords[loc + 2] = z;
      }

      public void set(int i, Geo g) {
         set(i, g.x(), g.y(), g.z());
      }

      public void set(int i, double lat, double lon, boolean isDegrees) {
         set(i, new Geo(lat, lon, isDegrees));
      }

      public Geo get(int i) {
         return get(i, new Geo());
      }

      public Geo get(int i, Geo ret) {

         if (ret == null) {
            ret = new Geo();
         }

         int loc = i * 3;
         double x = coords[loc];
         double y = coords[loc + 1];
         double z = coords[loc + 2];
         ret.initialize(x, y, z);
         return ret;
      }

      public boolean equals(int index, Geo comp) {
         int loc = index * 3;
         double x = coords[loc];
         double y = coords[loc + 1];
         double z = coords[loc + 2];
         return x == comp.x() && y == comp.y() && z == comp.z();
      }

      /**
       * Ensure that the Geo array starts and ends with the same values. Will
       * replace the current coord array with one three double longer if needed.
       */
      public void closeArray() {
         int l = coords.length;
         int i = l - 3;
         if (coords[0] != coords[i] || coords[1] != coords[i + 1] || coords[2] != coords[i + 2]) {
            double[] newCoords = new double[l + 3];
            System.arraycopy(coords, 0, newCoords, 0, l);
            newCoords[l] = coords[0];
            newCoords[l + 1] = coords[1];
            newCoords[l + 2] = coords[2];
            coords = newCoords;
         }
      }

      /**
       * Modify, if needed, the Geo array with the duplicates removed.
       */
      public void removeDups() {
         Geo[] ga = toPointArray();
         Geo[] r = new Geo[ga.length];
         int p = 0;
         for (int i = 0; i < ga.length; i++) {
            if (p == 0 || !(r[p - 1].equals(ga[i]))) {
               r[p] = ga[i];
               p++;
            }
         }

         if (p != ga.length) {
            coords = new double[p * 3];
            for (int i = 0; i < p; i++) {
               int loc = i * 3;
               Geo geo = r[i];
               coords[loc] = geo.x();
               coords[loc + 1] = geo.y();
               coords[loc + 2] = geo.z();
            }
         }
      }
   }
}
